<!DOCTYPE html>
<html>
  <head>
      <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta content="yes" name="apple-mobile-web-app-capable" />
  <meta content="black" name="apple-mobile-web-app-status-bar-style" />
  <meta name="referrer" content="never">
  <meta name="keywords" content="">
  <meta name="description" content="">
  <meta name="author" content="kveln">
  <title>K8s 持久化存储流程 | John Wong&#39;s Blog</title>
  <link href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
  <!-- <link href="https://blog.jwangkun.com/media/css/bootstrap.min.css" rel="stylesheet"> -->
  <!--  <link href="https://blog.jwangkun.com/media/css/all.min.css" rel="stylesheet" type="text/css"> -->
  <link href="https://cdn.bootcss.com/font-awesome/5.11.2/css/all.min.css" rel="stylesheet">
  <link href='https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic' rel='stylesheet' type='text/css'>
  <link href='https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/css'>
  <link rel="alternate" type="application/rss+xml" title="K8s 持久化存储流程 | John Wong&#39;s Blog » Feed" href="https://blog.jwangkun.com/atom.xml">
  <link rel="stylesheet"href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.15.10/build/styles/androidstudio.min.css">
  <link href="https://blog.jwangkun.com/styles/main.css" rel="stylesheet">
  <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
  <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.15.10/build/highlight.min.js"></script>
  <!-- <script src="https://blog.jwangkun.com/media/scripts/jquery.min.js"></script> -->
  <script>hljs.initHighlightingOnLoad();</script>
  

    <meta property="og:description" content="K8s 持久化存储流程"/>
    <meta property="og:url" content="https://blog.jwangkun.com/post/ctkbBNw6F/"/>
    <meta property="og:locale" content="zh-CN"/>
    <meta property="og:type" content="website"/>
    <meta property="og:site_name" content="John Wong&#39;s Blog"/>
  </head>
  <body>
  	<!-- Navigation -->
  <nav class="navbar navbar-expand-lg navbar-light fixed-top" id="mainNav">
    <div class="container">
      <a class="navbar-brand" href="https://blog.jwangkun.com">John Wong&#39;s Blog</a>
      <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
        Menu
        <i class="fas fa-bars"></i>
      </button>
      <div class="collapse navbar-collapse" id="navbarResponsive">
        <ul class="navbar-nav ml-auto">
          
          <li class="nav-item">
              
              <a class="nav-link" href="/">首页</a>
              
          </li>
          
          <li class="nav-item">
              
              <a class="nav-link" href="/archives">归档</a>
              
          </li>
          
          <li class="nav-item">
              
              <a class="nav-link" href="/tags">标签</a>
              
          </li>
          
          <li class="nav-item">
              
              <a class="nav-link" href="/post/about">关于</a>
              
          </li>
          
        </ul>
      </div>
    </div>
  </nav>
  <!-- Page Header -->
  <header class="masthead" style="background-image: url('https://blog.jwangkun.com/media/images/home-bg.jpg')">
    <div class="overlay"></div>
    <div class="container">
      <div class="row">
        <div class="col-lg-8 col-md-10 mx-auto">
          <div class="post-heading">
          	<span class="tags">
          	 
            <a href="https://blog.jwangkun.com/tag/ffAuwwMqh/" class="tag">存储</a>
            
            <a href="https://blog.jwangkun.com/tag/efVGHoi38/" class="tag">Kubernetes</a>
            
        </span>
            <h1>K8s 持久化存储流程</h1>
            <span class="meta">
            	Posted on
              2020-10-12，18 min read
            </span>
          </div>
        </div>
      </div>
    </div>
  </header>

  <!-- Post Content -->
  <article>
    <div class="container">
      <div class="row">
        <div class="col-lg-8 col-md-10 mx-auto">
          <p>导读：众所周知，K8s 的持久化存储（Persistent Storage）保证了应用数据独立于应用生命周期而存在，但其内部实现却少有人提及。K8s 内部的存储流程到底是怎样的？PV、PVC、StorageClass、Kubelet、CSI 插件等之间的调用关系又如何，这些谜底将在本文中一一揭晓。</p>
<h1 id="k8s-持久化存储基础">K8s 持久化存储基础</h1>
<p>在进行 K8s 存储流程讲解之前，先回顾一下 K8s 中持久化存储的基础概念。</p>
<h2 id="1-名词解释">1. 名词解释</h2>
<ul>
<li><strong>in-tree</strong>：代码逻辑在 K8s 官方仓库中；</li>
<li><strong>out-of-tree</strong>：代码逻辑在 K8s 官方仓库之外，实现与 K8s 代码的解耦；</li>
<li><strong>PV</strong>：PersistentVolume，集群级别的资源，由 集群管理员 or External Provisioner 创建。PV 的生命周期独立于使用 PV 的 Pod，PV 的 .Spec 中保存了存储设备的详细信息；</li>
<li><strong>PVC</strong>：PersistentVolumeClaim，命名空间（namespace）级别的资源，由 用户 or StatefulSet 控制器（根据 VolumeClaimTemplate） 创建。PVC 类似于 Pod，Pod 消耗 Node 资源，PVC 消耗 PV 资源。Pod 可以请求特定级别的资源（CPU 和内存），而 PVC 可以请求特定存储卷的大小及访问模式（Access Mode）；</li>
<li><strong>StorageClass</strong>：StorageClass 是集群级别的资源，由集群管理员创建。SC 为管理员提供了一种动态提供存储卷的 “类” 模板，SC 中的 .Spec 中详细定义了存储卷 PV 的不同服务质量级别、备份策略等等；</li>
<li><strong>CSI</strong>：Container Storage Interface，目的是定义行业标准的 “容器存储接口”，使存储供应商（SP）基于 CSI 标准开发的插件可以在不同容器编排（CO）系统中工作，CO 系统包括 Kubernetes、Mesos、Swarm 等。</li>
</ul>
<h2 id="2-组件介绍">2. 组件介绍</h2>
<ul>
<li><strong>PV Controller</strong>：负责 PV/PVC 绑定及周期管理，根据需求进行数据卷的 **Provision/Delete ** 操作；</li>
<li><strong>AD Controller</strong>：负责数据卷的 **Attach/Detach ** 操作，将设备挂接到目标节点；</li>
<li>Kubelet：Kubelet 是在每个 Node 节点上运行的主要 “节点代理”，功能是 Pod 生命周期管理、容器健康检查、容器监控等；</li>
<li><strong>Volume Manager</strong>：Kubelet 中的组件，负责管理数据卷的 **Mount/Umount ** 操作（也负责数据卷的 **Attach/Detach ** 操作，需配置 kubelet 相关参数开启该特性）、卷设备的格式化等等；</li>
<li><strong>Volume Plugins</strong>：存储插件，由存储供应商开发，目的在于扩展各种存储类型的卷管理能力，实现第三方存储的各种操作能力，即是<strong>上面蓝色操作</strong>的实现。Volume Plugins 有 in-tree 和 out-of-tree 两种；</li>
<li><strong>External Provioner</strong>：External Provioner 是一种 sidecar 容器，作用是调用 Volume Plugins 中的 CreateVolume 和 DeleteVolume 函数来执行 **Provision/Delete ** 操作。因为 K8s 的 PV 控制器无法直接调用 Volume Plugins 的相关函数，故由 External Provioner 通过 gRPC 来调用；</li>
<li><strong>External Attacher</strong>：External Attacher 是一种 sidecar 容器，作用是调用 Volume Plugins 中的 ControllerPublishVolume 和 ControllerUnpublishVolume 函数来执行 **Attach/Detach ** 操作。因为 K8s 的 AD 控制器无法直接调用 Volume Plugins 的相关函数，故由 External Attacher 通过 gRPC 来调用。</li>
</ul>
<h2 id="3-持久卷使用">3. 持久卷使用</h2>
<p>Kubernetes 为了使应用程序及其开发人员能够正常请求存储资源，<strong>避免处理存储设施细节</strong>，引入了 PV 和 PVC。创建 PV 有两种方式：</p>
<ul>
<li>一种是集群管理员通过手动方式<strong>静态创建</strong>应用所需要的 PV；</li>
<li>另一种是用户手动创建 PVC 并由 Provisioner 组件<strong>动态创建</strong>对应的 PV。</li>
</ul>
<p>下面我们以 NFS 共享存储为例来看二者区别。</p>
<h3 id="静态创建存储卷">静态创建存储卷</h3>
<p>静态创建存储卷流程如下图所示：</p>
<figure data-type="image" tabindex="1"><img src="https://user-gold-cdn.xitu.io/2020/4/10/1716296e746038bb?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img" loading="lazy"></figure>
<p><strong>第一步</strong>：集群管理员创建 NFS PV，NFS 属于 K8s 原生支持的 in-tree 存储类型。yaml 文件如下：</p>
<pre><code>apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 192.168.4.1
    path: /nfs_storage
复制代码
</code></pre>
<p><strong>第二步</strong>：用户创建 PVC，yaml 文件如下：</p>
<pre><code>apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
复制代码
</code></pre>
<p>通过 kubectl get pv 命令可看到 PV 和 PVC 已绑定：</p>
<pre><code>[root@huizhi ~]# kubectl get pvc
NAME      STATUS   VOLUME               CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nfs-pvc   Bound    nfs-pv-no-affinity   10Gi       RWO                           4s
复制代码
</code></pre>
<p><strong>第三步</strong>：用户创建应用，并使用第二步创建的 PVC。</p>
<pre><code>apiVersion: v1
kind: Pod
metadata:
  name: test-nfs
spec:
  containers:
  - image: nginx:alpine
    imagePullPolicy: IfNotPresent
    name: nginx
    volumeMounts:
    - mountPath: /data
      name: nfs-volume
  volumes:
  - name: nfs-volume
    persistentVolumeClaim:
      claimName: nfs-pvc
复制代码
</code></pre>
<p>此时 NFS 的远端存储就挂载了到 Pod 中 nginx 容器的 /data 目录下。</p>
<h3 id="动态创建存储卷">动态创建存储卷</h3>
<p>动态创建存储卷，要求集群中部署有 **nfs-client-provisioner ** 以及对应的 <strong>storageclass</strong>。</p>
<p>动态创建存储卷相比静态创建存储卷，少了集群管理员的干预，流程如下图所示：</p>
<figure data-type="image" tabindex="2"><img src="https://user-gold-cdn.xitu.io/2020/4/10/1716296e73f0450c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img" loading="lazy"></figure>
<p>集群管理员只需要保证环境中有 NFS 相关的 storageclass 即可：</p>
<pre><code>kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: nfs-sc
provisioner: example.com/nfs
mountOptions:
  - vers=4.1
复制代码
</code></pre>
<p><strong>第一步</strong>：用户创建 PVC，此处 PVC 的 storageClassName 指定为上面 NFS 的 storageclass 名称：</p>
<pre><code>kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: nfs
  annotations:
    volume.beta.kubernetes.io/storage-class: &quot;example-nfs&quot;
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Mi
  storageClassName: nfs-sc
复制代码
</code></pre>
<p><strong>第二步</strong>：集群中的 nfs-client-provisioner 会动态创建相应 PV。此时可看到环境中 PV 已创建，并与 PVC 已绑定。</p>
<pre><code>[root@huizhi ~]# kubectl get pv
NAME                                       CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM         REASON    AGE
pvc-dce84888-7a9d-11e6-b1ee-5254001e0c1b   10Mi        RWX           Delete          Bound       default/nfs             4s
复制代码
</code></pre>
<p><strong>第三步</strong>：用户创建应用，并使用第二步创建的 PVC，同静态创建存储卷的第三步。</p>
<h1 id="k8s-持久化存储流程">K8s 持久化存储流程</h1>
<h2 id="1-流程概览">1. 流程概览</h2>
<p>此处借鉴 <strong>@郡宝</strong> 在<a href="https://edu.aliyun.com/lesson_1651_13092#_13092">云原生存储课程</a>中的流程图</p>
<figure data-type="image" tabindex="3"><img src="https://user-gold-cdn.xitu.io/2020/4/10/1716296e7694ca1c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img" loading="lazy"></figure>
<p><strong>流程如下：</strong></p>
<ol>
<li>用户创建了一个包含 PVC 的 Pod，该 PVC 要求使用动态存储卷；</li>
<li>**Scheduler ** 根据 Pod 配置、节点状态、PV 配置等信息，把 Pod 调度到一个合适的 Worker 节点上；</li>
<li>**PV 控制器 **watch 到该 Pod 使用的 PVC 处于 Pending 状态，于是调用 <strong>Volume Plugin</strong>（in-tree）创建存储卷，并创建 PV 对象（out-of-tree 由 External Provisioner 来处理）；</li>
<li><strong>AD 控制器</strong>发现 Pod 和 PVC 处于待挂接状态，于是调用 **Volume Plugin ** 挂接存储设备到目标 Worker 节点上</li>
<li>在 Worker 节点上，<strong>Kubelet 中的 Volume Manager *<em>等待存储设备挂接完成，并通过 **Volume Plugin **将设备挂载到**全局目录**：*</em>/var/lib/kubelet/pods/[pod uid]/volumes/kubernetes.io~iscsi/[PV name]</strong>（以 iscsi 为例）；</li>
<li>**Kubelet ** 通过 Docker 启动 <strong>Pod 的 Containers</strong>，用 <strong>bind mount **方式将已挂载到本地全局目录的卷映射到**容器</strong>中。</li>
</ol>
<p><strong>更详细的流程如下：</strong></p>
<figure data-type="image" tabindex="4"><img src="https://user-gold-cdn.xitu.io/2020/4/10/1716296e76d400e0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img" loading="lazy"></figure>
<h2 id="2-流程详解">2. 流程详解</h2>
<p>不同 K8s 版本，持久化存储流程略有区别。本文基于 Kubernetes 1.14.8 版本。</p>
<p>从上述流程图中可看到，存储卷从创建到提供应用使用共分为三个阶段：<strong>Provision/Delete、Attach/Detach、Mount/Unmount。</strong></p>
<h3 id="provisioning-volumes">provisioning volumes</h3>
<figure data-type="image" tabindex="5"><img src="https://user-gold-cdn.xitu.io/2020/4/10/1716296e78d66584?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img" loading="lazy"></figure>
<p><strong>PV 控制器中有两个 Worker：</strong></p>
<ul>
<li><strong>ClaimWorker</strong>：处理 PVC 的 add / update / delete 相关事件以及 PVC 的状态迁移；</li>
<li><strong>VolumeWorker</strong>：负责 PV 的状态迁移。</li>
</ul>
<p><strong>PV 状态迁移（UpdatePVStatus）：</strong></p>
<ul>
<li>PV 初始状态为 Available，当 PV 与 PVC 绑定后，状态变为 Bound；</li>
<li>与 PV 绑定的 PVC 删除后，状态变为 Released；</li>
<li>当 PV 回收策略为 Recycled 或手动删除 PV 的 .Spec.ClaimRef 后，PV 状态变为 Available；</li>
<li>当 PV 回收策略未知或 Recycle 失败或存储卷删除失败，PV 状态变为 Failed；</li>
<li>手动删除 PV 的 .Spec.ClaimRef，PV 状态变为 Available。</li>
</ul>
<p><strong>PVC 状态迁移（UpdatePVCStatus）：</strong></p>
<ul>
<li>当集群中不存在满足 PVC 条件的 PV 时，PVC 状态为 Pending。在 PV 与 PVC 绑定后，PVC 状态由 Pending 变为 Bound；</li>
<li>与 PVC 绑定的 PV 在环境中被删除，PVC 状态变为 Lost；</li>
<li>再次与一个 ** 同名 PV ** 绑定后，PVC 状态变为 Bound。</li>
</ul>
<p><strong>Provisioning 流程如下（此处模拟用户创建一个新 PVC）：</strong></p>
<p><strong>静态存储卷流程（FindBestMatch）</strong>：<strong>PV 控制器</strong>首先在环境中筛选一个状态为 Available 的 PV 与新 PVC 匹配。</p>
<ul>
<li><strong>DelayBinding</strong>：PV 控制器判断该 PVC 是否需要 ** 延迟绑定：**1. 查看 PVC 的 annotation 中是否包含 volume.kubernetes.io/selected-node，若存在则表示该 PVC 已经被调度器指定好了节点（属于 <strong>ProvisionVolume</strong>），故不需要延迟绑定；2. 若 PVC 的 annotation 中不存在 volume.kubernetes.io/selected-node，同时没有 StorageClass，默认表示不需要延迟绑定；若有 StorageClass，查看其 VolumeBindingMode 字段，若为 WaitForFirstConsumer 则需要延迟绑定，若为 Immediate 则不需要延迟绑定；</li>
<li><strong>FindBestMatchPVForClaim</strong>：PV 控制器尝试找一个满足 PVC 要求的环境中现有的 PV。PV 控制器会将 ** 所有的 PV ** 进行一次筛选，并会从满足条件的 PV 中选择一个最佳匹配的 PV。筛选规则：1. VolumeMode 是否匹配；2. PV 是否已绑定到 PVC 上；3. PV 的 .Status.Phase 是否为 Available；4. LabelSelector 检查，PV 与 PVC 的 label 要保持一致；5. PV 与 PVC 的 StorageClass 是否一致；6. 每次迭代更新最小满足 PVC requested size 的 PV，并作为最终结果返回；</li>
<li><strong>Bind</strong>：PV 控制器对选中的 PV、PVC 进行绑定：1. 更新 PV 的 .Spec.ClaimRef 信息为当前 PVC；2. 更新 PV 的 .Status.Phase 为 Bound；3. 新增 PV 的 annotation ： pv.kubernetes.io/bound-by-controller: &quot;yes&quot;；4. 更新 PVC 的 .Spec.VolumeName 为 PV 名称；5. 更新 PVC 的 .Status.Phase 为 Bound；6. 新增 PVC 的 annotation：pv.kubernetes.io/bound-by-controller: &quot;yes&quot; 和 pv.kubernetes.io/bind-completed: &quot;yes&quot;；</li>
</ul>
<p>** 动态存储卷流程（ProvisionVolume）：** 若环境中没有合适的 PV，则进入动态 Provisioning 场景：</p>
<ul>
<li><strong>Before Provisioning</strong>：1. PV 控制器首先判断 PVC 使用的 StorageClass 是 in-tree 还是 out-of-tree：通过查看 StorageClass 的 Provisioner 字段是否包含 **&quot;kubernetes.io/&quot; ** 前缀来判断；2. PV 控制器更新 PVC 的 annotation：claim.Annotations[&quot;volume.beta.kubernetes.io/storage-provisioner&quot;] = storageClass.Provisioner；</li>
<li>**in-tree Provisioning（internal provisioning）：**1. in-tree 的 Provioner 会实现 ProvisionableVolumePlugin 接口的 NewProvisioner 方法，用来返回一个新的 Provisioner；2. PV 控制器调用 Provisioner 的 Provision 函数，该函数会返回一个 PV 对象；3. PV 控制器创建上一步返回的 PV 对象，将其与 PVC 绑定，Spec.ClaimRef 设置为 PVC，.Status.Phase 设置为 Bound，.Spec.StorageClassName 设置为与 PVC 相同的 StorageClassName；同时新增 annotation：&quot;pv.kubernetes.io/bound-by-controller&quot;=&quot;yes&quot; 和 &quot;pv.kubernetes.io/provisioned-by&quot;=plugin.GetPluginName()；</li>
<li>**out-of-tree Provisioning（external provisioning）：**1. External Provisioner 检查 PVC 中的 claim.Spec.VolumeName 是否为空，不为空则直接跳过该 PVC；2. External Provisioner 检查 PVC 中的 claim.Annotations[&quot;volume.beta.kubernetes.io/storage-provisioner&quot;] 是否等于自己的 Provisioner Name（External Provisioner 在启动时会传入 --provisioner 参数来确定自己的 Provisioner Name）；3. 若 PVC 的 VolumeMode=Block，检查 External Provisioner 是否支持块设备；4. External Provisioner 调用 Provision 函数：通过 gRPC 调用 CSI 存储插件的 CreateVolume 接口；5. External Provisioner 创建一个 PV 来代表该 volume，同时将该 PV 与之前的 PVC 做绑定。</li>
</ul>
<h3 id="deleting-volumes">deleting volumes</h3>
<p><strong>Deleting 流程为 Provisioning 的反操作：</strong></p>
<p>用户删除 PVC，删除 PV 控制器改变 PV.Status.Phase 为 Released。</p>
<p>当 PV.Status.Phase == Released 时，PV 控制器首先检查 Spec.PersistentVolumeReclaimPolicy 的值，为 Retain 时直接跳过，为 Delete 时：</p>
<ul>
<li>**in-tree Deleting：**1. in-tree 的 Provioner 会实现 DeletableVolumePlugin 接口的 NewDeleter 方法，用来返回一个新的 Deleter；2. 控制器调用 Deleter 的 Delete 函数，删除对应 volume；3. 在 volume 删除后，PV 控制器会删除 PV 对象；</li>
<li>**out-of-tree Deleting：**1. External Provisioner 调用 Delete 函数，通过 gRPC 调用 CSI 插件的 DeleteVolume 接口；2. 在 volume 删除后，External Provisioner 会删除 PV 对象</li>
</ul>
<h3 id="attaching-volumes">Attaching Volumes</h3>
<p>Kubelet 组件和 AD 控制器都可以做 attach/detach 操作，若 Kubelet 的启动参数中指定了 --enable-controller-attach-detach，则由 Kubelet 来做；否则默认由 AD 控制起来做。下面以 AD 控制器为例来讲解 attach/detach 操作。</p>
<figure data-type="image" tabindex="6"><img src="https://user-gold-cdn.xitu.io/2020/4/10/1716296ebe2f9bdc?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img" loading="lazy"></figure>
<p><strong>AD 控制器中有两个核心变量：</strong></p>
<ul>
<li><strong>DesiredStateOfWorld（DSW）</strong>：集群中预期的数据卷挂接状态，包含了 nodes-&gt;volumes-&gt;pods 的信息；</li>
<li><strong>ActualStateOfWorld（ASW）</strong>：集群中实际的数据卷挂接状态，包含了 volumes-&gt;nodes 的信息。</li>
</ul>
<p><strong>Attaching 流程如下：</strong></p>
<p>AD 控制器根据集群中的资源信息，初始化 DSW 和 ASW。</p>
<p>AD 控制器内部有三个组件周期性更新 DSW 和 ASW：</p>
<ul>
<li><strong>Reconciler。</strong> 通过一个 GoRoutine 周期性运行，<strong>确保 volume 挂接 / 摘除完毕</strong>。此期间不断更新 ASW：</li>
</ul>
<p>**in-tree attaching：**1. in-tree 的 Attacher 会实现 AttachableVolumePlugin 接口的 NewAttacher 方法，用来返回一个新的 Attacher；2. AD 控制器调用 Attacher 的 Attach 函数进行设备挂接；3. 更新 ASW。</p>
<p>**out-of-tree attaching：**1. 调用 in-tree 的 CSIAttacher 创建一个 <strong>VolumeAttachement（VA）</strong> 对象，该对象包含了 Attacher 信息、节点名称、待挂接 PV 信息；2. External Attacher 会 watch 集群中的 VolumeAttachement 资源，发现有需要挂接的数据卷时，调用 Attach 函数，通过 gRPC 调用 CSI 插件的 ControllerPublishVolume 接口。</p>
<ul>
<li>**DesiredStateOfWorldPopulator。*<em>通过一个 GoRoutine 周期性运行，主要功能是*<em>更新 DSW：</em></em></li>
</ul>
<p>findAndRemoveDeletedPods - 遍历所有 DSW 中的 Pods，若其已从集群中删除则从 DSW 中移除； findAndAddActivePods - 遍历所有 PodLister 中的 Pods，若 DSW 中不存在该 Pod 则添加至 DSW。</p>
<ul>
<li>**PVC Worker。**watch PVC 的 add/update 事件，处理 PVC 相关的 Pod，并实时更新 DSW。</li>
</ul>
<h3 id="detaching-volumes">Detaching Volumes</h3>
<p><strong>Detaching 流程如下：</strong></p>
<ul>
<li>当 Pod 被删除，AD 控制器会 watch 到该事件。首先 AD 控制器检查 Pod 所在的 Node 资源是否包含 &quot;volumes.kubernetes.io/keep-terminated-pod-volumes&quot; 标签，若包含则不做操作；不包含则从 DSW 中去掉该 volume；</li>
<li>AD 控制器通过 **Reconciler ** 使 ActualStateOfWorld 状态向 DesiredStateOfWorld 状态靠近，当发现 ASW 中有 DSW 中不存在的 volume 时，会做 Detach 操作：</li>
</ul>
<p>**in-tree detaching：**1. AD 控制器会实现 AttachableVolumePlugin 接口的 NewDetacher 方法，用来返回一个新的 Detacher；2. 控制器调用 Detacher 的 Detach 函数，detach 对应 volume；3. AD 控制器更新 ASW。</p>
<p>**out-of-tree detaching：**1. AD 控制器调用 in-tree 的 CSIAttacher 删除相关 VolumeAttachement 对象；2. External Attacher 会 watch 集群中的 VolumeAttachement（VA）资源，发现有需要摘除的数据卷时，调用 Detach 函数，通过 gRPC 调用 CSI 插件的 ControllerUnpublishVolume 接口；3. AD 控制器更新 ASW。</p>
<figure data-type="image" tabindex="7"><img src="https://user-gold-cdn.xitu.io/2020/4/10/1716296ebf9e6949?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img" loading="lazy"></figure>
<p>**Volume Manager ** 中同样也有两个核心变量：</p>
<ul>
<li><strong>DesiredStateOfWorld（DSW）</strong>：集群中预期的数据卷挂载状态，包含了 volumes-&gt;pods 的信息；</li>
<li><strong>ActualStateOfWorld（ASW）</strong>：集群中实际的数据卷挂载状态，包含了 volumes-&gt;pods 的信息。</li>
</ul>
<p><strong>Mounting/UnMounting 流程如下：</strong></p>
<p>全局目录（global mount path）存在的目的：块设备在 Linux 上只能挂载一次，而在 K8s 场景中，一个 PV 可能被挂载到同一个 Node 上的多个 Pod 实例中。若块设备格式化后先挂载至 Node 上的一个临时全局目录，然后再使用 Linux 中的 bind mount 技术把这个全局目录挂载进 Pod 中对应的目录上，就可以满足要求。上述流程图中，全局目录即 /var/lib/kubelet/pods/[pod uid]/volumes/kubernetes.io~iscsi/[PV name]</p>
<p>VolumeManager 根据集群中的资源信息，初始化 DSW 和 ASW。</p>
<p>VolumeManager 内部有两个组件周期性更新 DSW 和 ASW：</p>
<ul>
<li><strong>DesiredStateOfWorldPopulator</strong>：通过一个 GoRoutine 周期性运行，主要功能是<strong>更新 DSW；</strong></li>
<li><strong>Reconciler</strong>：通过一个 GoRoutine 周期性运行，<strong>确保 volume 挂载 / 卸载完毕</strong>。此期间不断更新 ASW：</li>
</ul>
<p><strong>unmountVolumes</strong>：确保 Pod 删除后 volumes 被 unmount。遍历一遍所有 ASW 中的 Pod，若其不在 DSW 中（表示 Pod 被删除），此处以 VolumeMode=FileSystem 举例，则执行如下操作：</p>
<ol>
<li>Remove all bind-mounts：调用 Unmounter 的 TearDown 接口（若为 out-of-tree 则调用 CSI 插件的 NodeUnpublishVolume 接口）；</li>
<li>Unmount volume：调用 DeviceUnmounter 的 UnmountDevice 函数（若为 out-of-tree 则调用 CSI 插件的 NodeUnstageVolume 接口）；</li>
<li>更新 ASW。</li>
</ol>
<p><strong>mountAttachVolumes</strong>：确保 Pod 要使用的 volumes 挂载成功。遍历一遍所有 DSW 中的 Pod，若其不在 ASW 中（表示目录待挂载映射到 Pod 上），此处以 VolumeMode=FileSystem 举例，执行如下操作：</p>
<ol>
<li>等待 volume 挂接到节点上（由 External Attacher or Kubelet 本身挂接）；</li>
<li>挂载 volume 到全局目录：调用 DeviceMounter 的 MountDevice 函数（若为 out-of-tree 则调用 CSI 插件的 NodeStageVolume 接口）；</li>
<li>更新 ASW：该 volume 已挂载到全局目录；</li>
<li>bind-mount volume 到 Pod 上：调用 Mounter 的 SetUp 接口（若为 out-of-tree 则调用 CSI 插件的 NodePublishVolume 接口）；</li>
<li>更新 ASW。</li>
</ol>
<p><strong>unmountDetachDevices</strong>：确保需要 unmount 的 volumes 被 unmount。遍历一遍所有 ASW 中的 UnmountedVolumes，若其不在 DSW 中（表示 volume 已无需使用），执行如下操作：</p>
<ol>
<li>Unmount volume：调用 DeviceUnmounter 的 UnmountDevice 函数（若为 out-of-tree 则调用 CSI 插件的 NodeUnstageVolume 接口）；</li>
<li>更新 ASW。</li>
</ol>
<h1 id="总结">总结</h1>
<p>本文先对 K8s 持久化存储基础概念及使用方法进行了介绍，并对 K8s 内部存储流程进行了深度解析。在 K8s 上，使用任何一种存储都离不开上面的流程（有些场景不会用到 attach/detach），环境上的存储问题也一定是其中某个环节出现了故障。</p>
<p>容器存储的坑比较多，专有云环境下尤其如此。不过挑战越多，机遇也越多！目前国内专有云市场在存储领域也是群雄逐鹿，我们敏捷 PaaS 容器团队欢迎大侠的加入，一起共创未来！</p>
<p>原文链接：https://juejin.im/post/6844904120353030158</p>

          
          <p class="next-post">下一篇：
            <a href="https://blog.jwangkun.com/post/7BntkM5Xr/">
              <span class="post-title">
                 使用 helm 部署 k8s 资源&rarr;
              </span>
            </a>
          </p>
        
        <div class="comment">
          
        </div>
      </div>
    </div>
  </article>
 <!-- Footer -->
  <footer>
    <div class="container">
      <div class="row">
        <div class="col-lg-8 col-md-10 mx-auto">
          <ul class="list-inline text-center">
            
            
              
            
              
            
              
            
              
            
              
            
              
            
              
              <li class="list-inline-item">
              <a href="https://blog.jwangkun.com/atom.xml" target="_blank">
                <span class="fa-stack fa-lg">
                  <i class="fas fa-circle fa-stack-2x"></i>
                  <i class="fas fa-rss fa-stack-1x fa-inverse"></i>
                </span>
              </a>
              </li>
          </ul>
          <p class="copyright text-muted">Copyright &copy;<span>John Wong&#39;s Blog</span><br><a href="https://github.com/getgridea/gridea" class="Themeinfo">Powered by Gridea</a></p>
        </div>
      </div>
    </div>
   </footer>
  <!-- Bootstrap core JavaScript -->
  <script src="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/js/bootstrap.bundle.min.js"></script>
  <!-- <script src="https://blog.jwangkun.com/media/scripts/bootstrap.bundle.min.js"></script> -->
  <!-- Bootstrap core JavaScript -->
  <script src="https://cdn.jsdelivr.net/gh/Alanrk/clean-cdn@1.0/scripts/clean-blog.min.js"></script>
  <!-- <script src="https://blog.jwangkun.com/media/scripts/clean-blog.min.js"></script> -->
  <script src="//instant.page/3.0.0" type="module" defer integrity="sha384-OeDn4XE77tdHo8pGtE1apMPmAipjoxUQ++eeJa6EtJCfHlvijigWiJpD7VDPWXV1"></script>
  <style type="text/css">a.back_to_top{text-decoration:none;position:fixed;bottom:40px;right:30px;background:#f0f0f0;height:40px;width:40px;border-radius:50%;line-height:36px;font-size:18px;text-align:center;transition-duration:.5s;transition-propety:background-color;display:none}a.back_to_top span{color:#888}a.back_to_top:hover{cursor:pointer;background:#dfdfdf}a.back_to_top:hover span{color:#555}@media print,screen and(max-width:580px){.back_to_top{display:none!important}}</style>
<a id="back_to_top" href="#" class="back_to_top">
  <span>▲</span></a>
<script>$(document).ready((function(_this) {
    return function() {
      var bt;
      bt = $('#back_to_top');
      if ($(document).width() > 480) {
        $(window).scroll(function() {
          var st;
          st = $(window).scrollTop();
          if (st > 30) {
            return bt.css('display', 'block')
          } else {
            return bt.css('display', 'none')
          }
        });
        return bt.click(function() {
          $('body,html').animate({
            scrollTop: 0
          },
          800);
          return false
        })
      }
    }
  })(this));
  var _hmt = _hmt || [];
(function() {
  var hm = document.createElement("script");
  hm.src = "https://hm.baidu.com/hm.js?84ab85460bfbe79dbe5776a1df139a8f";
  var s = document.getElementsByTagName("script")[0]; 
  s.parentNode.insertBefore(hm, s);
})();
  </script>
  
<script type="text/javascript" src="https://v1.cnzz.com/z_stat.php?id=1279350888&web_id=1279350888"></script>

  </body>
</html>

